<?php

/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */

/**
 * This file contains the Session_Store_DB class.
 *
 * PHP version 5
 *
 * This file is part of Majic - A rapid-development web application framework
 * for PHP 5.
 *
 * Majic is free software: you can redistribute it and/or modify it under
 * the terms of the GNU Lesser General Public License as published by the Free
 * Software Foundation, either version 3 of the License, or (at your option)
 * any later version.
 *
 * Majic is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
 * details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with Majic. If not, see <http://www.gnu.org/licenses/>.
 *
 * @package     Session
 * @author      Michael J. I. Jackson <mjijackson@gmail.com>
 * @copyright   2007-2008 Michael J. I. Jackson
 * @license     http://www.gnu.org/licenses/lgpl-3.0.txt GNU LGPL 3.0
 * @version     SVN: $Id: DB.php 34 2008-07-19 04:50:53Z mjijackson $
 */

/**
 * Enables storing of session data in a database. The database table should
 * have the following format:
 *
 * <code>
 * CREATE TABLE `sessions` (
 *     `id` CHAR(32) NOT NULL,
 *     `expiry` INT UNSIGNED NOT NULL DEFAULT 0,
 *     `data` TEXT NOT NULL,
 *     PRIMARY KEY (`id`)
 * );
 * </code>
 *
 * @package     Session
 * @author      Michael J. I. Jackson <mjijackson@gmail.com>
 */
class Session_Store_DB extends Session_Store
{

    /**
     * The database connection handle for this store.
     *
     * @var     DB
     * @access  private
     */
    private $db;

    /**
     * The table name for this store. Defaults to "sessions". The structure of
     * this table should be the following:
     *
     *
     * @var     string
     * @access  private
     */
    private $table = 'sessions';

    /**
     * A hash that is derived from this store's data. Helps to determine if the
     * data has changed since the last read.
     *
     * @var     string
     * @access  private
     */
    private $hash;

    /**
     * Constructor.
     *
     * @param   DB          $db         The database handle for this store
     * @param   string      $table      (optional) The name of the database
     *                                  table to use for this store
     * @access  public
     */
    public function __construct(DB $db, $table = null)
    {
        parent::__construct();

        $this->db = $db;
        if ($table) {
            $this->setTable($table);
        }
    }

    /**
     * @see     Session_Store::open()
     */
    public function open($save_path, $name)
    {
        return true;
    }

    /**
     * @see     Session_Store::close()
     */
    public function close()
    {
        return true;
    }

    /**
     * @see     Session_Store::read()
     */
    public function read($id)
    {
        try {
            $tbl = $this->table;
            $id = $this->db->quote(md5($id));

            $query = sprintf("SELECT data FROM $tbl WHERE id = $id AND expiry >= %d", time());

            $result = $this->db->queryOne($query);
        } catch (DB_Exception $e) {
            return false;
        }

        $this->hash = $this->makeHash($result);

        return $result;
    }

    /**
     * @see     Session_Store::write()
     */
    public function write($id, $data)
    {
        try {
            $tbl = $this->table;
            $id = $this->db->quote(md5($id));
            $data = $this->db->quote($data);
            $lifetime = ini_get('session.gc_maxlifetime');

            if ($this->hash && $this->hash == $this->makeHash($data)) {
                // session untouched, no need to update blob column
                $query = sprintf("UPDATE $tbl SET expiry = %d WHERE id = $id AND expiry >= %d",
                    time() + $lifetime,
                    time());
            } else {
                $one = $this->db->queryOne("SELECT COUNT(id) FROM $tbl WHERE id = $id");

                if (intval($one) == 0) {
                    $query = sprintf("INSERT INTO $tbl (id, expiry, data) VALUES ($id, %d, $data)",
                        time() + $lifetime);
                } else {
                    $query  = sprintf("UPDATE $tbl SET expiry = %d, data = $data WHERE id = $id AND expiry >= %d",
                        time() + $lifetime,
                        time());
                }
            }

            $this->db->exec($query);
        } catch (DB_Exception $e) {
            return false;
        }

        return true;
    }

    /**
     * @see     Session_Store::destroy()
     */
    public function destroy($id)
    {
        try {
            $tbl = $this->table;
            $id = $this->db->quote(md5($id));

            $this->db->exec("DELETE FROM $tbl WHERE id = $id");
        } catch (DB_Exception $e) {
            return false;
        }

        return true;
    }

    /**
     * @see     Session_Store::gc()
     */
    public function gc($maxlifetime)
    {
        try {
            $tbl = $this->table;
            $query = sprintf("DELETE FROM $tbl WHERE expiry < %d", time());

            $this->db->exec($query);
        } catch (DB_Exception $e) {
            return false;
        }

        return true;
    }

    /**
     * Sets the name of the table to use for this store.
     *
     * @param   string
     * @return  void
     * @access  public
     */
    public function setTable($table)
    {
        $this->table = $table;
    }

    /**
     * Generates a unique hash from this store's serialized session data.
     *
     * @param   string  $data           The serialized session data
     * @return  string                  The session data hash
     * @access  private
     */
    private function makeHash($data)
    {
        return strlen($data) . crc32($data);
    }

}

?>
